﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel;
using System.Reflection;
using Cherry.Agile.ServiceModel.Config;
using System.Threading;
using Cherry.Agile.ServiceModel.Interceptor;
using Cherry.Agile.ServiceModel.Runtime;
using Cherry.Agile.Framework;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Cherry.Agile.ServiceModel.Routing {
    class ReadWriteServiceEndpointTable:IDisposable {

        Dictionary<string, ServiceEndpoint[]> _endpoints = new Dictionary<string, ServiceEndpoint[]>();
        long __readIndex = -1;
        Dictionary<string, ServerItem[]> _readOnlyAddress = new Dictionary<string, ServerItem[]>();
        Dictionary<string, ServerItem[]> _writeOnlyAddress = new Dictionary<string, ServerItem[]>();
        Dictionary<string, ServerItem> _backendServers = new Dictionary<string, ServerItem>();

        public ReadWriteServiceEndpointTable() {
            foreach (ServerGroup group in RempServiceModelSection.Current.Router.ServerGroups) {
                var rs = new List<ServerItem>();
                var ws = new List<ServerItem>();
                foreach (ServerElement item in group) {
                    string address = item.Address.TrimEnd('/') + "/";
                    var server = ServerItem.GetServerItem(address,
                        item.Rule, item.Usage == "write" ? ReadWriteUsage.WriteOnly : ReadWriteUsage.ReadOnly);
                    if (server.Usage == ReadWriteUsage.WriteOnly) {
                        if (ws.Count > 0)
                            throw new ArgumentException("write server can not add more than one.");
                        ws.Add(server);
                    } else {
                        rs.Add(server);
                    }
                    _backendServers.Add(server.Address, server);
                }
                if (ws.Count == 0)
                    throw new ArgumentException("must set one writeable server.");

                if (rs.Count == 0)
                    rs.Add(ws[0]);
                _readOnlyAddress.Add(group.Name, rs.ToArray());
                _writeOnlyAddress.Add(group.Name, ws.ToArray());
            }

            Monitor.MonitorService.BackendServerActive += new EventHandler<Monitor.MonitorNotifyEventArgs>(OnBackendServiceActivated);
            Monitor.MonitorService.BackendServerTearDown += new EventHandler<Monitor.MonitorNotifyEventArgs>(OnBackendServiceTearDown);
        }

        public void Load() {
            var path = System.AppDomain.CurrentDomain.BaseDirectory;
            var files = Directory.GetFiles(path, "*.interface.dll");
            foreach (var file in files) {
                var asm = Assembly.LoadFile(file);
                var contracts = asm.GetTypes().Where(c => RuntimeUnity.ValidateServiceInterface(c));
                foreach (var item in contracts) {
                    Load(item);
                }
            }
        }

        public void Load(Type contract) {
            //_contract = contract;
            var @namespace = GetServiceNameSpace(contract);
            CreateRreadWriteEndpoint(@namespace, contract);
        }

        private void CreateRreadWriteEndpoint(string @namespace, Type contract) {
            foreach (var mi in contract.GetMethods()) {
                var item = new ServiceEndpointItem(@namespace, mi);
                _endpoints.Add(item.ActionName, GetServiceEndpoints(contract, item));
            }
        }

        private string GetServiceNameSpace(Type t) {
            if (!t.IsInterface)
                throw new ArgumentException("invlidd type,the 't' must be a interface.");
            var atr = t.GetCustomAttributes(typeof(ServiceContractAttribute), false).FirstOrDefault() as ServiceContractAttribute;

            if (atr == null)
                throw new ArgumentException("the interface must add ServiceContractAttribute");

            return atr.Namespace.TrimEnd('/');
        }

        private void OnBackendServiceActivated(object sender ,Monitor.MonitorNotifyEventArgs e)
        {
            ServerItem item;
            if (_backendServers.TryGetValue(e.Addresses[0], out item)) {
                item.LastActiveDate = DateTime.Now;
                item.State = ServerState.Online;
                Trace.TraceInformation("router\t backend server {0} actived.", e.Addresses[0]);
            }
        }

        private void OnBackendServiceTearDown(object sender, Monitor.MonitorNotifyEventArgs e) {
            foreach (var address in e.Addresses) {
                ServerItem item;
                if (_backendServers.TryGetValue(address, out item)) {
                    item.State = ServerState.Offline;
                    Trace.TraceInformation("router\t backend server {0} offline.", address);
                }
            }
        }

        private ServiceEndpoint[] GetServiceEndpoints(Type contract, ServiceEndpointItem item) {
            List<ServiceEndpoint> _endpoints = new List<ServiceEndpoint>();
            var config = ClientConfigHelper.GetConfig(contract);
            var groupName = config.ServerGroup;
            if (string.IsNullOrEmpty(groupName))
                throw new ArgumentException("invlaid contract,please make sure serverGroup attribute is not null.");
            bool readOnly = false;

            if (item.Usage == ReadWriteUsage.ReadOnly || item.Usage == ReadWriteUsage.None) {
                ServerItem[] rs;
                if (!_readOnlyAddress.TryGetValue(groupName, out rs))
                    throw new ArgumentNullException("can not find serverGroup:" + groupName);

                foreach (var val in rs) {
                    if (val.Rule != null && !val.Rule.Match(item.OperationName))
                        continue;
                    var address = val.Address + config.GetAddress(contract);
                    var endpoint = new EndpointAddress(address);
                    _endpoints.Add(new ServiceEndpoint(ContractDescription.GetContract(contract),
                                        DefaultEndpointPolicy.EndpointPolicy.DefaultBinding, endpoint));
                    Trace.TraceInformation("routing\tload backen server readonly endpoint:" + address);
                    readOnly = true;
                }

            }
            if (!readOnly) {
                ServerItem[] ws;
                if (!_writeOnlyAddress.TryGetValue(groupName, out ws))
                    throw new ArgumentNullException("can not find serverGroup:" + groupName);
                var address = ws[0].Address + config.GetAddress(contract);
                var endpoint = new EndpointAddress(address);
                _endpoints.Add(new ServiceEndpoint(ContractDescription.GetContract(contract),
                                    DefaultEndpointPolicy.EndpointPolicy.DefaultBinding, endpoint));
                Trace.TraceInformation("routing\tload backen server writeonly endpoint:" + address);
            }
            return _endpoints.ToArray();
        }

        public ServiceEndpoint GetServiceEndpoint(string actionName) {
            ServiceEndpoint[] items;
            if (!_endpoints.TryGetValue(actionName, out items))
                throw new ArgumentException("invalid action name:" + actionName);

            var q = from c in items
                    from a in _backendServers.Values
                    where c.Address.Uri.AbsoluteUri.StartsWith(a.Address) 
                    && a.State == ServerState.Online
                    select c;

            var vals = q.ToArray();
            if (vals.Length == 0)
                throw new SystemException("can not find any available service.");
            var item = vals[Interlocked.Increment(ref __readIndex) % vals.Length];
            Trace.TraceInformation("routing\t{0} to {1}" ,actionName,item.Address.Uri);
            return item;
        }
        
        public void Dispose() {
            Monitor.MonitorService.BackendServerActive -= new EventHandler<Monitor.MonitorNotifyEventArgs>(OnBackendServiceActivated);
            Monitor.MonitorService.BackendServerTearDown -= new EventHandler<Monitor.MonitorNotifyEventArgs>(OnBackendServiceActivated);
        }
    }

    class ServerItem {
        static Dictionary<string, ServerItem> __dict = new Dictionary<string, ServerItem>();

        private ServerItem() {

        }

        public static ServerItem GetServerItem(string address, string rule, ReadWriteUsage usage) {
            ServerItem item;
            if (__dict.TryGetValue(address, out item))
                return item;
            item = new ServerItem();
            __dict.Add(address, item);
            item.Address = address;
            item.Usage = usage;
            if (!string.IsNullOrEmpty(rule))
                item.Rule = Rule.GetRule(rule);
            return item;
        }

        public string Address { get; private set; }

        public Rule Rule { get; set; }

        public ReadWriteUsage Usage { get; set; }

        public ServerState State { get; set; }

        public DateTime? LastActiveDate { get; set; }
    }

    class Rule {
        static Dictionary<string, Rule> __dict = new Dictionary<string, Rule>();
        Regex regex;
        private Rule() { }

        public static Rule GetRule(string ruleName) {
            Rule rule;
            if (__dict.TryGetValue(ruleName, out rule))
                return rule;
            rule = new Rule();
            __dict.Add(ruleName, rule);
            rule.Name = ruleName;
            var item = RempServiceModelSection.Current.Router.Rules.Cast<RuleElement>().Where(c => c.Name == ruleName).FirstOrDefault();
            if (item == null)
                throw new ArgumentNullException("could not find any rule name is " + ruleName);
            rule.Type = item.Type;
            rule.Value = item.Value;

            rule.regex = new Regex(rule.Value);
            return rule;
        }

        public string Name { get; private set; }

        public string Type { get; private set; }

        public string Value { get; private set; }

        public bool Match(string operation) {
            return regex.Match(operation).Success;
        }
    }

    enum ServerState {
        Online,
        Offline
    }

    class ServiceEndpointItem {

        public string NameSpace { get; private set; }

        public string ActionName { get; private set; }

        public string OperationName { get; private set; }

        public ReadWriteUsage Usage { get; private set; }

        public ServiceEndpointItem(string nameSpace, MethodInfo mi) {
            var op = mi.GetCustomAttributes(typeof(OperationContractAttribute), false).FirstOrDefault() as OperationContractAttribute;
            if (op == null)
                throw new ArgumentException();

            NameSpace = nameSpace;

            ActionName = op.Action;

            if (string.IsNullOrEmpty(ActionName)) {
                var name = OperationName = op.Name ?? mi.Name;
                var t = mi.DeclaringType.GetCustomAttributes(typeof(ServiceContractAttribute), false).FirstOrDefault() as ServiceContractAttribute;
                var contractName = t.Name ?? mi.DeclaringType.Name;
                ActionName = string.Format("{0}/{1}/{2}", nameSpace, contractName, name);
            }
            var rw = mi.GetCustomAttributes(typeof(OperationReadWriteAttribute), false).FirstOrDefault() as OperationReadWriteAttribute;

            Usage = rw != null ? rw.Usage : ReadWriteUsage.None;

        }
    }

}
